/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text;

import java.awt.event.ActionEvent;
import java.io.*;
import java.text.*;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.*;
import javax.swing.*;

/**
 * <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
 * using an instance of <code>java.text.Format</code> to handle the
 * conversion to a String, and the conversion from a String.
 * <p>
 * If <code>getAllowsInvalid()</code> is false, this will ask the
 * <code>Format</code> to format the current text on every edit.
 * <p>
 * You can specify a minimum and maximum value by way of the
 * <code>setMinimum</code> and <code>setMaximum</code> methods. In order
 * for this to work the values returned from <code>stringToValue</code> must be
 * comparable to the min/max values by way of the <code>Comparable</code>
 * interface.
 * <p>
 * Be careful how you configure the <code>Format</code> and the
 * <code>InternationalFormatter</code>, as it is possible to create a
 * situation where certain values can not be input. Consider the date
 * format 'M/d/yy', an <code>InternationalFormatter</code> that is always
 * valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
 * (<code>setOverwriteMode(true)</code>) and the date 7/1/99. In this
 * case the user will not be able to enter a two digit month or day of
 * month. To avoid this, the format should be 'MM/dd/yy'.
 * <p>
 * If <code>InternationalFormatter</code> is configured to only allow valid
 * values (<code>setAllowsInvalid(false)</code>), every valid edit will result
 * in the text of the <code>JFormattedTextField</code> being completely reset
 * from the <code>Format</code>.
 * The cursor position will also be adjusted as literal characters are
 * added/removed from the resulting String.
 * <p>
 * <code>InternationalFormatter</code>'s behavior of
 * <code>stringToValue</code> is  slightly different than that of
 * <code>DefaultTextFormatter</code>, it does the following:
 * <ol>
 * <li><code>parseObject</code> is invoked on the <code>Format</code>
 * specified by <code>setFormat</code>
 * <li>If a Class has been set for the values (<code>setValueClass</code>),
 * supers implementation is invoked to convert the value returned
 * from <code>parseObject</code> to the appropriate class.
 * <li>If a <code>ParseException</code> has not been thrown, and the value
 * is outside the min/max a <code>ParseException</code> is thrown.
 * <li>The value is returned.
 * </ol>
 * <code>InternationalFormatter</code> implements <code>stringToValue</code>
 * in this manner so that you can specify an alternate Class than
 * <code>Format</code> may return.
 * <p>
 * <strong>Warning:</strong>
 * Serialized objects of this class will not be compatible with
 * future Swing releases. The current serialization support is
 * appropriate for short term storage or RMI between applications running
 * the same version of Swing.  As of 1.4, support for long term storage
 * of all JavaBeans&trade;
 * has been added to the <code>java.beans</code> package.
 * Please see {@link java.beans.XMLEncoder}.
 *
 * @see java.text.Format
 * @see java.lang.Comparable
 * @since 1.4
 */
public class InternationalFormatter extends DefaultFormatter {

  /**
   * Used by <code>getFields</code>.
   */
  private static final Format.Field[] EMPTY_FIELD_ARRAY = new Format.Field[0];

  /**
   * Object used to handle the conversion.
   */
  private Format format;
  /**
   * Can be used to impose a maximum value.
   */
  private Comparable max;
  /**
   * Can be used to impose a minimum value.
   */
  private Comparable min;

  /**
   * <code>InternationalFormatter</code>'s behavior is dicatated by a
   * <code>AttributedCharacterIterator</code> that is obtained from
   * the <code>Format</code>. On every edit, assuming
   * allows invalid is false, the <code>Format</code> instance is invoked
   * with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
   * also kept upto date with the non-literal characters, that is
   * for every index in the <code>AttributedCharacterIterator</code> an
   * entry in the bit set is updated based on the return value from
   * <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
   * this cached information.
   * <p>
   * If allowsInvalid is false, every edit results in resetting the complete
   * text of the JTextComponent.
   * <p>
   * InternationalFormatterFilter can also provide two actions suitable for
   * incrementing and decrementing. To enable this a subclass must
   * override <code>getSupportsIncrement</code> to return true, and
   * override <code>adjustValue</code> to handle the changing of the
   * value. If you want to support changing the value outside of
   * the valid FieldPositions, you will need to override
   * <code>canIncrement</code>.
   */
  /**
   * A bit is set for every index identified in the
   * AttributedCharacterIterator that is not considered decoration.
   * This should only be used if validMask is true.
   */
  private transient BitSet literalMask;
  /**
   * Used to iterate over characters.
   */
  private transient AttributedCharacterIterator iterator;
  /**
   * True if the Format was able to convert the value to a String and
   * back.
   */
  private transient boolean validMask;
  /**
   * Current value being displayed.
   */
  private transient String string;
  /**
   * If true, DocumentFilter methods are unconditionally allowed,
   * and no checking is done on their values. This is used when
   * incrementing/decrementing via the actions.
   */
  private transient boolean ignoreDocumentMutate;


  /**
   * Creates an <code>InternationalFormatter</code> with no
   * <code>Format</code> specified.
   */
  public InternationalFormatter() {
    setOverwriteMode(false);
  }

  /**
   * Creates an <code>InternationalFormatter</code> with the specified
   * <code>Format</code> instance.
   *
   * @param format Format instance used for converting from/to Strings
   */
  public InternationalFormatter(Format format) {
    this();
    setFormat(format);
  }

  /**
   * Sets the format that dictates the legal values that can be edited
   * and displayed.
   *
   * @param format <code>Format</code> instance used for converting from/to Strings
   */
  public void setFormat(Format format) {
    this.format = format;
  }

  /**
   * Returns the format that dictates the legal values that can be edited
   * and displayed.
   *
   * @return Format instance used for converting from/to Strings
   */
  public Format getFormat() {
    return format;
  }

  /**
   * Sets the minimum permissible value. If the <code>valueClass</code> has
   * not been specified, and <code>minimum</code> is non null, the
   * <code>valueClass</code> will be set to that of the class of
   * <code>minimum</code>.
   *
   * @param minimum Minimum legal value that can be input
   * @see #setValueClass
   */
  public void setMinimum(Comparable minimum) {
    if (getValueClass() == null && minimum != null) {
      setValueClass(minimum.getClass());
    }
    min = minimum;
  }

  /**
   * Returns the minimum permissible value.
   *
   * @return Minimum legal value that can be input
   */
  public Comparable getMinimum() {
    return min;
  }

  /**
   * Sets the maximum permissible value. If the <code>valueClass</code> has
   * not been specified, and <code>max</code> is non null, the
   * <code>valueClass</code> will be set to that of the class of
   * <code>max</code>.
   *
   * @param max Maximum legal value that can be input
   * @see #setValueClass
   */
  public void setMaximum(Comparable max) {
    if (getValueClass() == null && max != null) {
      setValueClass(max.getClass());
    }
    this.max = max;
  }

  /**
   * Returns the maximum permissible value.
   *
   * @return Maximum legal value that can be input
   */
  public Comparable getMaximum() {
    return max;
  }

  /**
   * Installs the <code>DefaultFormatter</code> onto a particular
   * <code>JFormattedTextField</code>.
   * This will invoke <code>valueToString</code> to convert the
   * current value from the <code>JFormattedTextField</code> to
   * a String. This will then install the <code>Action</code>s from
   * <code>getActions</code>, the <code>DocumentFilter</code>
   * returned from <code>getDocumentFilter</code> and the
   * <code>NavigationFilter</code> returned from
   * <code>getNavigationFilter</code> onto the
   * <code>JFormattedTextField</code>.
   * <p>
   * Subclasses will typically only need to override this if they
   * wish to install additional listeners on the
   * <code>JFormattedTextField</code>.
   * <p>
   * If there is a <code>ParseException</code> in converting the
   * current value to a String, this will set the text to an empty
   * String, and mark the <code>JFormattedTextField</code> as being
   * in an invalid state.
   * <p>
   * While this is a public method, this is typically only useful
   * for subclassers of <code>JFormattedTextField</code>.
   * <code>JFormattedTextField</code> will invoke this method at
   * the appropriate times when the value changes, or its internal
   * state changes.
   *
   * @param ftf JFormattedTextField to format for, may be null indicating uninstall from current
   * JFormattedTextField.
   */
  public void install(JFormattedTextField ftf) {
    super.install(ftf);
    updateMaskIfNecessary();
    // invoked again as the mask should now be valid.
    positionCursorAtInitialLocation();
  }

  /**
   * Returns a String representation of the Object <code>value</code>.
   * This invokes <code>format</code> on the current <code>Format</code>.
   *
   * @param value Value to convert
   * @return String representation of value
   * @throws ParseException if there is an error in the conversion
   */
  public String valueToString(Object value) throws ParseException {
    if (value == null) {
      return "";
    }
    Format f = getFormat();

    if (f == null) {
      return value.toString();
    }
    return f.format(value);
  }

  /**
   * Returns the <code>Object</code> representation of the
   * <code>String</code> <code>text</code>.
   *
   * @param text <code>String</code> to convert
   * @return <code>Object</code> representation of text
   * @throws ParseException if there is an error in the conversion
   */
  public Object stringToValue(String text) throws ParseException {
    Object value = stringToValue(text, getFormat());

    // Convert to the value class if the Value returned from the
    // Format does not match.
    if (value != null && getValueClass() != null &&
        !getValueClass().isInstance(value)) {
      value = super.stringToValue(value.toString());
    }
    try {
      if (!isValidValue(value, true)) {
        throw new ParseException("Value not within min/max range", 0);
      }
    } catch (ClassCastException cce) {
      throw new ParseException("Class cast exception comparing values: "
          + cce, 0);
    }
    return value;
  }

  /**
   * Returns the <code>Format.Field</code> constants associated with
   * the text at <code>offset</code>. If <code>offset</code> is not
   * a valid location into the current text, this will return an
   * empty array.
   *
   * @param offset offset into text to be examined
   * @return Format.Field constants associated with the text at the given position.
   */
  public Format.Field[] getFields(int offset) {
    if (getAllowsInvalid()) {
      // This will work if the currently edited value is valid.
      updateMask();
    }

    Map<Attribute, Object> attrs = getAttributes(offset);

    if (attrs != null && attrs.size() > 0) {
      ArrayList<Attribute> al = new ArrayList<Attribute>();

      al.addAll(attrs.keySet());
      return al.toArray(EMPTY_FIELD_ARRAY);
    }
    return EMPTY_FIELD_ARRAY;
  }

  /**
   * Creates a copy of the DefaultFormatter.
   *
   * @return copy of the DefaultFormatter
   */
  public Object clone() throws CloneNotSupportedException {
    InternationalFormatter formatter = (InternationalFormatter) super.
        clone();

    formatter.literalMask = null;
    formatter.iterator = null;
    formatter.validMask = false;
    formatter.string = null;
    return formatter;
  }

  /**
   * If <code>getSupportsIncrement</code> returns true, this returns
   * two Actions suitable for incrementing/decrementing the value.
   */
  protected Action[] getActions() {
    if (getSupportsIncrement()) {
      return new Action[]{new IncrementAction("increment", 1),
          new IncrementAction("decrement", -1)};
    }
    return null;
  }

  /**
   * Invokes <code>parseObject</code> on <code>f</code>, returning
   * its value.
   */
  Object stringToValue(String text, Format f) throws ParseException {
    if (f == null) {
      return text;
    }
    return f.parseObject(text);
  }

  /**
   * Returns true if <code>value</code> is between the min/max.
   *
   * @param wantsCCE If false, and a ClassCastException is thrown in comparing the values, the
   * exception is consumed and false is returned.
   */
  boolean isValidValue(Object value, boolean wantsCCE) {
    Comparable min = getMinimum();

    try {
      if (min != null && min.compareTo(value) > 0) {
        return false;
      }
    } catch (ClassCastException cce) {
      if (wantsCCE) {
        throw cce;
      }
      return false;
    }

    Comparable max = getMaximum();
    try {
      if (max != null && max.compareTo(value) < 0) {
        return false;
      }
    } catch (ClassCastException cce) {
      if (wantsCCE) {
        throw cce;
      }
      return false;
    }
    return true;
  }

  /**
   * Returns a Set of the attribute identifiers at <code>index</code>.
   */
  Map<Attribute, Object> getAttributes(int index) {
    if (isValidMask()) {
      AttributedCharacterIterator iterator = getIterator();

      if (index >= 0 && index <= iterator.getEndIndex()) {
        iterator.setIndex(index);
        return iterator.getAttributes();
      }
    }
    return null;
  }


  /**
   * Returns the start of the first run that contains the attribute
   * <code>id</code>. This will return <code>-1</code> if the attribute
   * can not be found.
   */
  int getAttributeStart(AttributedCharacterIterator.Attribute id) {
    if (isValidMask()) {
      AttributedCharacterIterator iterator = getIterator();

      iterator.first();
      while (iterator.current() != CharacterIterator.DONE) {
        if (iterator.getAttribute(id) != null) {
          return iterator.getIndex();
        }
        iterator.next();
      }
    }
    return -1;
  }

  /**
   * Returns the <code>AttributedCharacterIterator</code> used to
   * format the last value.
   */
  AttributedCharacterIterator getIterator() {
    return iterator;
  }

  /**
   * Updates the AttributedCharacterIterator and bitset, if necessary.
   */
  void updateMaskIfNecessary() {
    if (!getAllowsInvalid() && (getFormat() != null)) {
      if (!isValidMask()) {
        updateMask();
      } else {
        String newString = getFormattedTextField().getText();

        if (!newString.equals(string)) {
          updateMask();
        }
      }
    }
  }

  /**
   * Updates the AttributedCharacterIterator by invoking
   * <code>formatToCharacterIterator</code> on the <code>Format</code>.
   * If this is successful,
   * <code>updateMask(AttributedCharacterIterator)</code>
   * is then invoked to update the internal bitmask.
   */
  void updateMask() {
    if (getFormat() != null) {
      Document doc = getFormattedTextField().getDocument();

      validMask = false;
      if (doc != null) {
        try {
          string = doc.getText(0, doc.getLength());
        } catch (BadLocationException ble) {
          string = null;
        }
        if (string != null) {
          try {
            Object value = stringToValue(string);
            AttributedCharacterIterator iterator = getFormat().
                formatToCharacterIterator(value);

            updateMask(iterator);
          } catch (ParseException pe) {
          } catch (IllegalArgumentException iae) {
          } catch (NullPointerException npe) {
          }
        }
      }
    }
  }

  /**
   * Returns the number of literal characters before <code>index</code>.
   */
  int getLiteralCountTo(int index) {
    int lCount = 0;

    for (int counter = 0; counter < index; counter++) {
      if (isLiteral(counter)) {
        lCount++;
      }
    }
    return lCount;
  }

  /**
   * Returns true if the character at index is a literal, that is
   * not editable.
   */
  boolean isLiteral(int index) {
    if (isValidMask() && index < string.length()) {
      return literalMask.get(index);
    }
    return false;
  }

  /**
   * Returns the literal character at index.
   */
  char getLiteral(int index) {
    if (isValidMask() && string != null && index < string.length()) {
      return string.charAt(index);
    }
    return (char) 0;
  }

  /**
   * Returns true if the character at offset is navigable too. This
   * is implemented in terms of <code>isLiteral</code>, subclasses
   * may wish to provide different behavior.
   */
  boolean isNavigatable(int offset) {
    return !isLiteral(offset);
  }

  /**
   * Overriden to update the mask after invoking supers implementation.
   */
  void updateValue(Object value) {
    super.updateValue(value);
    updateMaskIfNecessary();
  }

  /**
   * Overriden to unconditionally allow the replace if
   * ignoreDocumentMutate is true.
   */
  void replace(DocumentFilter.FilterBypass fb, int offset,
      int length, String text,
      AttributeSet attrs) throws BadLocationException {
    if (ignoreDocumentMutate) {
      fb.replace(offset, length, text, attrs);
      return;
    }
    super.replace(fb, offset, length, text, attrs);
  }

  /**
   * Returns the index of the next non-literal character starting at
   * index. If index is not a literal, it will be returned.
   *
   * @param direction Amount to increment looking for non-literal
   */
  private int getNextNonliteralIndex(int index, int direction) {
    int max = getFormattedTextField().getDocument().getLength();

    while (index >= 0 && index < max) {
      if (isLiteral(index)) {
        index += direction;
      } else {
        return index;
      }
    }
    return (direction == -1) ? 0 : max;
  }

  /**
   * Overriden in an attempt to honor the literals.
   * <p>If we do not allow invalid values and are in overwrite mode, this
   * {@code rh.length} is corrected as to preserve trailing literals.
   * If not in overwrite mode, and there is text to insert it is
   * inserted at the next non literal index going forward.  If there
   * is only text to remove, it is removed from the next non literal
   * index going backward.
   */
  boolean canReplace(ReplaceHolder rh) {
    if (!getAllowsInvalid()) {
      String text = rh.text;
      int tl = (text != null) ? text.length() : 0;
      JTextComponent c = getFormattedTextField();

      if (tl == 0 && rh.length == 1 && c.getSelectionStart() != rh.offset) {
        // Backspace, adjust to actually delete next non-literal.
        rh.offset = getNextNonliteralIndex(rh.offset, -1);
      } else if (getOverwriteMode()) {
        int pos = rh.offset;
        int textPos = pos;
        boolean overflown = false;

        for (int i = 0; i < rh.length; i++) {
          while (isLiteral(pos)) {
            pos++;
          }
          if (pos >= string.length()) {
            pos = textPos;
            overflown = true;
            break;
          }
          textPos = ++pos;
        }
        if (overflown || c.getSelectedText() == null) {
          rh.length = pos - rh.offset;
        }
      } else if (tl > 0) {
        // insert (or insert and remove)
        rh.offset = getNextNonliteralIndex(rh.offset, 1);
      } else {
        // remove only
        rh.offset = getNextNonliteralIndex(rh.offset, -1);
      }
      ((ExtendedReplaceHolder) rh).endOffset = rh.offset;
      ((ExtendedReplaceHolder) rh).endTextLength = (rh.text != null) ?
          rh.text.length() : 0;
    } else {
      ((ExtendedReplaceHolder) rh).endOffset = rh.offset;
      ((ExtendedReplaceHolder) rh).endTextLength = (rh.text != null) ?
          rh.text.length() : 0;
    }
    boolean can = super.canReplace(rh);
    if (can && !getAllowsInvalid()) {
      ((ExtendedReplaceHolder) rh).resetFromValue(this);
    }
    return can;
  }

  /**
   * When in !allowsInvalid mode the text is reset on every edit, thus
   * supers implementation will position the cursor at the wrong position.
   * As such, this invokes supers implementation and then invokes
   * <code>repositionCursor</code> to correctly reset the cursor.
   */
  boolean replace(ReplaceHolder rh) throws BadLocationException {
    int start = -1;
    int direction = 1;
    int literalCount = -1;

    if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
        (getFormattedTextField().getSelectionStart() != rh.offset ||
            rh.length > 1)) {
      direction = -1;
    }
    if (!getAllowsInvalid()) {
      if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) {
        // remove
        start = getFormattedTextField().getSelectionStart();
      } else {
        start = rh.offset;
      }
      literalCount = getLiteralCountTo(start);
    }
    if (super.replace(rh)) {
      if (start != -1) {
        int end = ((ExtendedReplaceHolder) rh).endOffset;

        end += ((ExtendedReplaceHolder) rh).endTextLength;
        repositionCursor(literalCount, end, direction);
      } else {
        start = ((ExtendedReplaceHolder) rh).endOffset;
        if (direction == 1) {
          start += ((ExtendedReplaceHolder) rh).endTextLength;
        }
        repositionCursor(start, direction);
      }
      return true;
    }
    return false;
  }

  /**
   * Repositions the cursor. <code>startLiteralCount</code> gives
   * the number of literals to the start of the deleted range, end
   * gives the ending location to adjust from, direction gives
   * the direction relative to <code>end</code> to position the
   * cursor from.
   */
  private void repositionCursor(int startLiteralCount, int end,
      int direction) {
    int endLiteralCount = getLiteralCountTo(end);

    if (endLiteralCount != end) {
      end -= startLiteralCount;
      for (int counter = 0; counter < end; counter++) {
        if (isLiteral(counter)) {
          end++;
        }
      }
    }
    repositionCursor(end, 1 /*direction*/);
  }

  /**
   * Returns the character from the mask that has been buffered
   * at <code>index</code>.
   */
  char getBufferedChar(int index) {
    if (isValidMask()) {
      if (string != null && index < string.length()) {
        return string.charAt(index);
      }
    }
    return (char) 0;
  }

  /**
   * Returns true if the current mask is valid.
   */
  boolean isValidMask() {
    return validMask;
  }

  /**
   * Returns true if <code>attributes</code> is null or empty.
   */
  boolean isLiteral(Map attributes) {
    return ((attributes == null) || attributes.size() == 0);
  }

  /**
   * Updates the interal bitset from <code>iterator</code>. This will
   * set <code>validMask</code> to true if <code>iterator</code> is
   * non-null.
   */
  private void updateMask(AttributedCharacterIterator iterator) {
    if (iterator != null) {
      validMask = true;
      this.iterator = iterator;

      // Update the literal mask
      if (literalMask == null) {
        literalMask = new BitSet();
      } else {
        for (int counter = literalMask.length() - 1; counter >= 0;
            counter--) {
          literalMask.clear(counter);
        }
      }

      iterator.first();
      while (iterator.current() != CharacterIterator.DONE) {
        Map attributes = iterator.getAttributes();
        boolean set = isLiteral(attributes);
        int start = iterator.getIndex();
        int end = iterator.getRunLimit();

        while (start < end) {
          if (set) {
            literalMask.set(start);
          } else {
            literalMask.clear(start);
          }
          start++;
        }
        iterator.setIndex(start);
      }
    }
  }

  /**
   * Returns true if <code>field</code> is non-null.
   * Subclasses that wish to allow incrementing to happen outside of
   * the known fields will need to override this.
   */
  boolean canIncrement(Object field, int cursorPosition) {
    return (field != null);
  }

  /**
   * Selects the fields identified by <code>attributes</code>.
   */
  void selectField(Object f, int count) {
    AttributedCharacterIterator iterator = getIterator();

    if (iterator != null &&
        (f instanceof AttributedCharacterIterator.Attribute)) {
      AttributedCharacterIterator.Attribute field =
          (AttributedCharacterIterator.Attribute) f;

      iterator.first();
      while (iterator.current() != CharacterIterator.DONE) {
        while (iterator.getAttribute(field) == null &&
            iterator.next() != CharacterIterator.DONE) {
          ;
        }
        if (iterator.current() != CharacterIterator.DONE) {
          int limit = iterator.getRunLimit(field);

          if (--count <= 0) {
            getFormattedTextField().select(iterator.getIndex(),
                limit);
            break;
          }
          iterator.setIndex(limit);
          iterator.next();
        }
      }
    }
  }

  /**
   * Returns the field that will be adjusted by adjustValue.
   */
  Object getAdjustField(int start, Map attributes) {
    return null;
  }

  /**
   * Returns the number of occurrences of <code>f</code> before
   * the location <code>start</code> in the current
   * <code>AttributedCharacterIterator</code>.
   */
  private int getFieldTypeCountTo(Object f, int start) {
    AttributedCharacterIterator iterator = getIterator();
    int count = 0;

    if (iterator != null &&
        (f instanceof AttributedCharacterIterator.Attribute)) {
      AttributedCharacterIterator.Attribute field =
          (AttributedCharacterIterator.Attribute) f;

      iterator.first();
      while (iterator.getIndex() < start) {
        while (iterator.getAttribute(field) == null &&
            iterator.next() != CharacterIterator.DONE) {
          ;
        }
        if (iterator.current() != CharacterIterator.DONE) {
          iterator.setIndex(iterator.getRunLimit(field));
          iterator.next();
          count++;
        } else {
          break;
        }
      }
    }
    return count;
  }

  /**
   * Subclasses supporting incrementing must override this to handle
   * the actual incrementing. <code>value</code> is the current value,
   * <code>attributes</code> gives the field the cursor is in (may be
   * null depending upon <code>canIncrement</code>) and
   * <code>direction</code> is the amount to increment by.
   */
  Object adjustValue(Object value, Map attributes, Object field,
      int direction) throws
      BadLocationException, ParseException {
    return null;
  }

  /**
   * Returns false, indicating InternationalFormatter does not allow
   * incrementing of the value. Subclasses that wish to support
   * incrementing/decrementing the value should override this and
   * return true. Subclasses should also override
   * <code>adjustValue</code>.
   */
  boolean getSupportsIncrement() {
    return false;
  }

  /**
   * Resets the value of the JFormattedTextField to be
   * <code>value</code>.
   */
  void resetValue(Object value) throws BadLocationException, ParseException {
    Document doc = getFormattedTextField().getDocument();
    String string = valueToString(value);

    try {
      ignoreDocumentMutate = true;
      doc.remove(0, doc.getLength());
      doc.insertString(0, string, null);
    } finally {
      ignoreDocumentMutate = false;
    }
    updateValue(value);
  }

  /**
   * Subclassed to update the internal representation of the mask after
   * the default read operation has completed.
   */
  private void readObject(ObjectInputStream s)
      throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    updateMaskIfNecessary();
  }


  /**
   * Overriden to return an instance of <code>ExtendedReplaceHolder</code>.
   */
  ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
      int length, String text,
      AttributeSet attrs) {
    if (replaceHolder == null) {
      replaceHolder = new ExtendedReplaceHolder();
    }
    return super.getReplaceHolder(fb, offset, length, text, attrs);
  }


  /**
   * As InternationalFormatter replaces the complete text on every edit,
   * ExtendedReplaceHolder keeps track of the offset and length passed
   * into canReplace.
   */
  static class ExtendedReplaceHolder extends ReplaceHolder {

    /**
     * Offset of the insert/remove. This may differ from offset in
     * that if !allowsInvalid the text is replaced on every edit.
     */
    int endOffset;
    /**
     * Length of the text. This may differ from text.length in
     * that if !allowsInvalid the text is replaced on every edit.
     */
    int endTextLength;

    /**
     * Resets the region to delete to be the complete document and
     * the text from invoking valueToString on the current value.
     */
    void resetFromValue(InternationalFormatter formatter) {
      // Need to reset the complete string as Format's result can
      // be completely different.
      offset = 0;
      try {
        text = formatter.valueToString(value);
      } catch (ParseException pe) {
        // Should never happen, otherwise canReplace would have
        // returned value.
        text = "";
      }
      length = fb.getDocument().getLength();
    }
  }


  /**
   * IncrementAction is used to increment the value by a certain amount.
   * It calls into <code>adjustValue</code> to handle the actual
   * incrementing of the value.
   */
  private class IncrementAction extends AbstractAction {

    private int direction;

    IncrementAction(String name, int direction) {
      super(name);
      this.direction = direction;
    }

    public void actionPerformed(ActionEvent ae) {

      if (getFormattedTextField().isEditable()) {
        if (getAllowsInvalid()) {
          // This will work if the currently edited value is valid.
          updateMask();
        }

        boolean validEdit = false;

        if (isValidMask()) {
          int start = getFormattedTextField().getSelectionStart();

          if (start != -1) {
            AttributedCharacterIterator iterator = getIterator();

            iterator.setIndex(start);

            Map attributes = iterator.getAttributes();
            Object field = getAdjustField(start, attributes);

            if (canIncrement(field, start)) {
              try {
                Object value = stringToValue(
                    getFormattedTextField().getText());
                int fieldTypeCount = getFieldTypeCountTo(
                    field, start);

                value = adjustValue(value, attributes,
                    field, direction);
                if (value != null && isValidValue(value, false)) {
                  resetValue(value);
                  updateMask();

                  if (isValidMask()) {
                    selectField(field, fieldTypeCount);
                  }
                  validEdit = true;
                }
              } catch (ParseException pe) {
              } catch (BadLocationException ble) {
              }
            }
          }
        }
        if (!validEdit) {
          invalidEdit();
        }
      }
    }
  }
}
